
package cz.czu.pef.DataModelling.dao.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.core.GenericTypeResolver;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import cz.czu.pef.DataModelling.dao.*;
import cz.czu.pef.DataModelling.entityObjects.IdPersistenceEntityObject;
import cz.czu.pef.common.CustomFilterField;
import cz.czu.pef.common.CustomSortField;

/**
 * Abstract class which implements shared basic DAO operations for given Entity.
 * 
 * @author Rostislav Stříbrný
 *
 * @param <T>
 * 
 * Inspiration:
 * - http://www.ibm.com/developerworks/java/library/j-typesafejpa/
 */
@Component
@Transactional
@Configurable
public abstract class AbstractDaoJpaImpl<T extends IdPersistenceEntityObject> implements Dao<T>
{

  private final Class<T> genericType;

  @SuppressWarnings("unchecked")
  public AbstractDaoJpaImpl()
  {
    this.genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractDaoJpaImpl.class);
  }

  private static final Logger logger = Logger.getLogger(AbstractDaoJpaImpl.class.getName());

  @PersistenceContext
  private EntityManager entityManager;

  public void setEntityManager(EntityManager entityManager)
  {
    this.entityManager = entityManager;
  }

  public EntityManager getEntityManager()
  {
    return this.entityManager;
  }

  public void remove(T entity)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("delete - going to delete entity: " + entity);
    }
    entityManager.remove(entity);
  }

  @SuppressWarnings("unchecked")
  private TypedQuery<T> getCriteriaQuerySelectByParameter(String paramName, Object paramValue)
  {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();

    CriteriaQuery<T> q = cb.createQuery(genericType);
    Root<T> c = q.from(genericType);
    ParameterExpression<Object> p = null;
    if (paramValue != null)
    {
    	p = (ParameterExpression<Object>) cb.parameter(paramValue.getClass());
    q.select(c).where(cb.equal(c.get(paramName), p));
    }
    else
    {
    	q.select(c).where(cb.isNull(c.get(paramName)));
    }

    TypedQuery<T> query = entityManager.createQuery(q);
    if (p != null)
    query.setParameter(p, paramValue);
    return query;
  }

  private TypedQuery<T> getCriteriaQuerySelectByParameterIn(String paramName, final Collection<? extends Object> values)
  {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();

    CriteriaQuery<T> q = cb.createQuery(genericType);
    Root<T> c = q.from(genericType);
    Predicate inPredicate = c.get(paramName).in(values);
    q.select(c).where(inPredicate);

    TypedQuery<T> query = entityManager.createQuery(q);
    return query;
  }

  private CriteriaQuery<T> getCriteriaQueryByCustomFields(List<CustomFilterField> filterFields, List<CustomSortField> sortFields)
  {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<T> criteria = builder.createQuery(genericType);
    Root<T> root = criteria.from(genericType);
    criteria.select(root);
    addFilterCriterions(criteria, root, filterFields);

    addSortCriterions(criteria, root, sortFields);
    return criteria;
  }

  @Override
  public void removeById(long idValue)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("removeById():: going to delete entity/entities with ID: " + idValue);
    }

    List<T> objsToDelete = getCriteriaQuerySelectByParameter("id", new Long(idValue)).getResultList();
    for (T obj : objsToDelete)
    {
      logger.info("removeById():: going to delete entity: " + obj);
      remove(obj);
    }
    logger.info("removeById():: done");
  }

  @Override
  public T getByStringId(String id)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("getByStringId - getting entity by id (String): " + id);
    }
    return (T) entityManager.find(genericType, Long.valueOf(id));
  }

  @Override
  public T getById(long id)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("getById - getting entity by id (long): " + id);
    }
    return (T) entityManager.find(genericType, new Long(id));
  }

  @Override
  public T getByIdLocked(long id)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("getByIdLocked - getting locked entity by id: " + id);
    }
    return (T) entityManager.find(genericType, new Long(id), LockModeType.WRITE);
  }

  @Override
  public void persist(T entity)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("persist - going to save / update entity: " + entity);
    }
    entityManager.persist(entity);
  }

  @Override
  public T merge(T entity)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("merge - going to merge entity: " + entity);
    }
    return entityManager.merge(entity);
  }

  @Override
  public T mergeUnique(final String attribName, final Object value, T entity)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("merge - going to merge entity: " + entity);
    }
    T uniqueEntity = findByAttributeUniqueResult(attribName, value);
    if (uniqueEntity != null)
    {
      // Copy unique ID so it will merge to the same object
      entity.setId(uniqueEntity.getId());
    }

    return entityManager.merge(entity);
  }

  @Override
  public T mergeUniqueExt(final String[] attribNames, final Object[] values, T entity)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("merge - going to merge entity: " + entity);
    }
    if (attribNames == null)
      throw new RuntimeException("attribNames is null");
    if (values == null)
      throw new RuntimeException("values is null");
    if (attribNames.length != values.length)
      throw new RuntimeException("attribNames.length != values.length");

    List<CustomFilterField> filterList = new ArrayList<CustomFilterField>();
    for (int i = 0; i < attribNames.length; i++)
    {
      CustomFilterField cff = new CustomFilterField();
      cff.setPropertyName(attribNames[i]);
      cff.setFilterValue(values[i].toString());
      filterList.add(cff);
    }

    List<T> found = this.findAllByRangeWithFilter(null, null, filterList);
    if (found != null)
    {
      if (found.size() > 1)
        throw new RuntimeException("Found more than one record: " + found.size());

      // Copy unique ID so it will merge to the same object
      entity.setId(found.get(0).getId());
    }

    return entityManager.merge(entity);
  }

  @Override
  public Long count()
  {
    return countWithFilter(null);
  }

  @Override
  public Long countWithFilter(List<CustomFilterField> filterFields)
  {
    logger.info("countWithFilter - going to count the entities");

    return findCountByCriteria(getCriteriaQueryByCustomFields(filterFields, null));
  }

  /**
   * Retrieves all entities of designated type
   *
   * @return
   */
  @Override
  public List<T> findAll()
  {
    logger.info("findAll():: Looking up for all entities");
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<T> criteria = builder.createQuery(genericType);
    Root<T> root = criteria.from(genericType);
    criteria.select(root);

    TypedQuery<T> query = entityManager.createQuery(criteria);
    return query.getResultList();
  }

  /**
   * Retrieves all entities of designated type limited between firstRow and endRow
   *
   * @return
   */
  @Override
  public List<T> findAllByRange(Integer firstRow, Integer endRow)
  {
    logger.info("findAllByRange():: Looking up for all entities between " + firstRow + " and " + endRow);
    return findAllByRangeWithFilterAndSort(firstRow, endRow, null, null);
  }

  /**
   * Retrieves all entities of designated type limited ((between firstRow and endRow) AND (filterFields (if any)))
   *
   * @return
   */
  @Override
  public List<T> findAllByRangeWithFilter(Integer firstRow, Integer endRow, List<CustomFilterField> filterFields)
  {
    logger.info("findAllByRangeWithFilter():: Looking up for all entities between " + firstRow + " and " + endRow);
    return findAllByRangeWithFilterAndSort(firstRow, endRow, filterFields, null);
  }


  /*
   * Retrieves all entities of designated type limited ((between firstRow and endRow) AND (filterFields (if any))).
   *     Ordered according to sortFields.
   *
   * @return
   */
  @Override
  public List<T> findAllByRangeWithFilterAndSort(Integer firstRow, Integer endRow, List<CustomFilterField> filterFields, List<CustomSortField> sortFields)
  {
    logger.info("findAllByRangeWithFilterAndSort():: Looking up for all entities between " + firstRow + " and " + endRow);
    CriteriaQuery<T> criteria = getCriteriaQueryByCustomFields(filterFields, sortFields);
    TypedQuery<T> query = entityManager.createQuery(criteria);

    if (firstRow != null && firstRow > 0)
    {
      query.setFirstResult(firstRow);
      if (endRow != null && endRow > 0)
        query.setMaxResults(endRow - firstRow + 1);
    }

    return query.getResultList();
  }

  /**
   * Retrieves all entities of designated type filtered by condition
   * attribName column == value
   *
   * @param attribName
   * @param value
   * @return
   */
  @Override
  public List<T> findByAttribute(final String attribName, final Object value)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("findByAttribute - looking up for all entities where: " + attribName + " = " + value);
    }
    return getCriteriaQuerySelectByParameter(attribName, value).getResultList();
  }

  /**
   * Retrieves one entity of designated type filtered by condition
   * attribName column == value
   *
   * @param attribName
   * @param value
   * @return
   */
  @Override
  public T findByAttributeUniqueResult(final String attribName, final Object value)
  {
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("findByAttribute - looking up for all entities where: " + attribName + " = " + value);
    }
    List<T> list = getCriteriaQuerySelectByParameter(attribName, value).getResultList();
    if (list.isEmpty())
      return null;
    return list.get(0);
  }

  /**
   * Retrieves all entities of designated type filtered by condition
   * attribName column in values
   *
   * @param attribName
   * @param value
   * @return
   */
  @Override
  public List<T> findByAttributeIn(final String attribName, final Collection<? extends Object> values)
  {
    if (values == null)
      throw new RuntimeException("values is null");
    if (logger.isLoggable(Level.INFO))
    {
      logger.info("findByAttributeIn - looking up for all entities where: " + attribName + " in " + values);
    }
    return getCriteriaQuerySelectByParameterIn(attribName, values).getResultList();
  }

  private void addFilterCriterions(CriteriaQuery<T> criteria, Root<T> root, List<CustomFilterField> filterFields)
  {
    Predicate predicate = null;
    if (filterFields != null)
    {
      CriteriaBuilder builder = entityManager.getCriteriaBuilder();
      for (CustomFilterField field : filterFields)
      {
        Predicate predicateLocal = null;
        if (field != null && field.getPropertyName() != null && field.getFilterValue() != null)
        {
          predicateLocal = builder.equal(root.get(field.getPropertyName()), field.getFilterValue());
        }
        if (predicateLocal != null)
        {
          if (predicate == null)
            predicate = predicateLocal;
          else
            predicate = builder.and(predicate, predicateLocal);
        }
        //crit.add(Restrictions.ilike(field.getPropertyName(), field.getFilterValue()));
      }
      if (predicate != null)
        criteria.where(predicate);
    }
  }

  private void addSortCriterions(CriteriaQuery<T> criteria, Root<T> root, List<CustomSortField> sortFields)
  {
    if (sortFields != null)
    {
      CriteriaBuilder builder = entityManager.getCriteriaBuilder();
      for (CustomSortField field : sortFields)
      {
        if (field != null && field.getPropertyName() != null)
        {
          if (CustomSortField.ASCENDING.equals(field.getOrdering()))
            criteria.orderBy(builder.asc(root.get(field.getPropertyName())));
          //crit.addOrder(Order.asc(field.getPropertyName()));
          if (CustomSortField.DESCENDING.equals(field.getOrdering()))
            criteria.orderBy(builder.desc(root.get(field.getPropertyName())));
          //crit.addOrder(Order.desc(field.getPropertyName()));
        }
      }
    }
  }

  /**
   * Counts the number of results of a search.
   * 
   * @param criteria The criteria for the query.
   * @return The number of results of the query.
   */
  private Long findCountByCriteria(CriteriaQuery<?> criteria)
  {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();

    CriteriaQuery<Long> countCriteria = builder.createQuery(Long.class);
    Root<?> entityRoot = countCriteria.from(criteria.getResultType());
    countCriteria.select(builder.count(entityRoot));
    if (criteria.getRestriction() != null)
      countCriteria.where(criteria.getRestriction());

    return entityManager.createQuery(countCriteria).getSingleResult();
  }
}
